Skip to content

fix(desktop): give tool rows the names, arguments and results the UI renders - #821

Merged
agentforce314 merged 1 commit into
mainfrom
fix/desktop-tool-vocabulary
Aug 9, 2026
Merged

fix(desktop): give tool rows the names, arguments and results the UI renders#821
agentforce314 merged 1 commit into
mainfrom
fix/desktop-tool-vocabulary

Conversation

@agentforce314

Copy link
Copy Markdown
Owner

Follow-up to #820. Tool rows still looked nothing like the TUI's: plain untitled entries, no icons, no diffs, and — for anything that returned text — no output at all.

Why

The desktop renderer keys every per-tool formatter off its own tool vocabulary (read_file, terminal, edit_file, …) and reads a tool's output from named result fields. The agent speaks Claude Code's vocabulary (Read, Bash, Edit) and returns plain text. Nothing matched:

  • no name in TOOL_META → the unlabelled generic path: no title, no icon, no tone
  • firstStringField(args, ['path','file','filepath']) never sees file_path
  • a plain-string result is discarded by the renderer's parseMaybeJsonObject, so finished tools rendered empty

What changed

All three are adapted in the gateway, mirroring the TUI's own summarization (ui-tui/src/gatewayClient.ts toolContext / formatToolResult) so both surfaces describe a run the same way.

before after
Read Read · (empty) Read file seed.py · file contents · "Read 2 lines"
Bash Bash · (empty) Ran command · $ ls src/ transcript · ANSI output
Edit Edit · (empty) Edited file a.py · rendered diff, "Added 1 line, removed 1 line"
Glob Glob · (empty) Listed files · the matches · 3 files chip
WebSearch WebSearch · (empty) Searched web · 3 results · 2.4s
failure Bash · (empty) error row · cleaned reason, no diff

Unknown tools (Task, MCP tools) pass through unchanged and keep the generic treatment, which is the right fallback.

Details worth calling out:

  • The single argument worth showing rides along as context in the TUI's priority order, so a Grep row shows its pattern rather than its search path. path is aliased in alongside file_path because that is the spelling the file formatters look for.
  • An edit's inline_diff is rebuilt from the structuredPatch display envelope, so it renders as a real diff with add/remove counts — the TUI treats the diff AS the result, and now so does the desktop.
  • Failure text has its model-facing markup (<tool_use_error>, <sandbox_violations>) stripped before display.

Testing

The wire payloads are frozen into gateway-tool-events.fixture.json, generated by the Python translator and rendered through the real renderer by the new ui-desktop/src/lib/gateway-tool-contract.test.ts, which asserts what a user actually sees.

That pairing is the point. This class of bug is invisible to either language alone — each side happily tests a field the other never reads, which is precisely how the blank rows shipped. Writing the test immediately caught two more live mismatches that the Python tests were happy with: the argument context outranking a Glob's matches on the renderer's generic path, and a diff that never reached the view.

  • Full Python suite: 9902 passed, 15 skipped
  • tsc --noEmit: clean
  • New contract test: 10 passed

🤖 Generated with Claude Code

…renders

Tool rows still looked nothing like the TUI's after the payload-shape fix:
plain untitled entries, no icons, no diffs, and — for anything that returned
text — no output at all. The renderer keys every per-tool formatter off its
own tool vocabulary (read_file/terminal/edit_file/…) and reads output from
named result fields, while the agent speaks Claude Code's (Read/Bash/Edit/…)
and returns plain text. Nothing matched, so every tool fell through to the
unlabelled generic path, and a plain-string result was dropped outright by
the renderer's parseMaybeJsonObject.

The gateway now adapts all three, mirroring the TUI's own summarization
(ui-tui/src/gatewayClient.ts toolContext/formatToolResult) so both surfaces
describe a run the same way:

- Names map onto the renderer's vocabulary, which is what turns on its
  titles ("Read file", "Ran command"), icons and tones. Unknown tools pass
  through and keep the generic treatment.
- The one argument worth showing rides along as `context`, in the TUI's
  priority order — a Grep row shows its pattern rather than its search path.
  `path` is aliased in alongside `file_path` because that is the spelling
  the file formatters look for.
- Results land where each tool family reads them: `content` for a read,
  `output` for a shell run, `inline_diff` for an edit — rebuilt from the
  structuredPatch display envelope so an edit renders as a real diff with
  add/remove counts, as it does in the TUI. Glob/Grep get a count chip, a
  web search its "3 results / 2.4s". Failures show a cleaned reason with
  the model-facing markup stripped, and no diff.

Testing: the wire payloads are frozen into a fixture that the Python suite
generates and ui-desktop's new gateway-tool-contract.test.ts renders through
the real renderer, asserting what a user actually sees. That pairing is the
point — this class of bug is invisible to either language alone, since each
side happily tests a field the other never reads. Writing it immediately
caught two more live mismatches: the argument context outranking a Glob's
matches, and a diff that never reached the view.

Full Python suite green; ui-desktop typecheck clean.

Co-Authored-By: Claude <noreply@anthropic.com>
@agentforce314
agentforce314 merged commit 2149d06 into main Aug 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant